Skip to content

Resolve and load each imported module once per transform - #124

Merged
nstepien merged 2 commits into
mainfrom
resolve-imports-once
Nov 4, 2026
Merged

nstepien merged 2 commits into
mainfrom
resolve-imports-once

Conversation

@nstepien

@nstepien nstepien commented Nov 4, 2026

Copy link
Copy Markdown
Owner

When a template interpolates values imported from another module, resolveValue resolved, loaded, and parsed that module again for every value, calling context.resolve and context.load each time. A transform now does this once per imported module, however many values it interpolates from it. The modules are keyed by importer and import source, as values can also come through the imports of an imported module.

Benchmarks

Medians with Vite 8.3.5, main against this branch. The JS, CSS, and final source maps are identical.

Workload main This branch
Vite build of a synthetic project: 40 components × 8 templates × 5 tokens from one tokens.ts 54.2 ms 32.8 ms (−39%)
Cold dev server transforms of those 40 components 71.1 ms 39.8 ms (−44%)
Vite build of react-data-grid's src, with 9 interpolations in 15 modules 21.9 ms 21.8 ms

A transform now records each dependency and checks isWaitingFor once per imported module instead of once per value.

The uncovered lines are unchanged: the tests reuse a loaded module 15 times. An import that can't be resolved stays untested, as on main.

#68 includes the same change (importedFiles), so whichever lands second needs to account for it.

AGENTS.md now says that a transform handles each imported module once.

🤖 Generated with Claude Code

A transform resolved, loaded, and parsed an imported module again for
every value it interpolated from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

loadImportedModule now returns the cached promise, and
resolveImportedModule does the resolving, loading, and parsing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nstepien
nstepien marked this pull request as ready for review November 4, 2026 02:00
@nstepien
nstepien merged commit 23f8859 into main Nov 4, 2026
12 checks passed
@nstepien
nstepien deleted the resolve-imports-once branch November 4, 2026 02:02
nstepien added a commit that referenced this pull request Nov 4, 2026
main reshaped the code this branch builds on since the last merge, so
the merge ports the branch onto main's design again:

- The scope visitor is shared across parses (#109), so the callbacks
  for raw TypeScript bindings and `export default` join it, and the
  default export is recorded through its visit state.
- Imported modules are resolved, loaded, and parsed once per transform
  through main's loadImportedModule (#124), which now also skips
  external modules, keys modules by moduleKey, and records dependencies
  by file.
- Modules are parsed as the language the bundler assigned them through
  main's parseLangOf (#125), which this branch had too.
- extractCssFromCode keeps main's single pass (#114), and hotUpdate its
  early return, with this branch's handling of query variants.
- readModuleCode moved to module level, as consistent-function-scoping
  is on since #109.

Tests: the inline snapshots of this branch's tests follow Rolldown's new
module comments (`// path` instead of `//#region path` and
`//#endregion`), with no other change. The dev server test of virtual
modules uses main's shared getCode and getStylesheets helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstepien added a commit that referenced this pull request Nov 7, 2026
The parse cache only stored finished parses. The transforms of the
modules that import the same module run concurrently: in a build, they
all wait for `this.load()` of that module and resume together, so each
one missed the cache and read and parsed the file again. In a project of
400 components that interpolate values of one `tokens.ts`, it was parsed
400 times per build, and in a cold dev server too.

The cache now holds the pending parse of each module read from disk,
stored as soon as it is requested, so the file is read and parsed once.
The parse of a module being transformed is stored as is. `parseFile`
only parses now, and `loadParsedFile` returns the cached or pending
parse of a module read from disk, like `loadImportedModule` does for the
imported modules of a transform (#124). Caching the promise is safe for
the same reason: a parse never waits for a value to resolve, so it can't
wait for itself.

**Benchmarks**

Medians with Vite 8.3.5, #127 against this branch. The JS, CSS, and
final source maps are identical.

| Workload | #127 | This branch |
|---|---|---|
| Vite build of a synthetic project: 40 components × 8 templates × 5
tokens from one `tokens.ts` | 28.5 ms | 23.1 ms (−19%) |
| The same with 400 components | 224.0 ms | 171.6 ms (−23%) |
| Cold dev server transforms of the 40 components | 35.9 ms | 31.8 ms
(−11%) |
| Cold dev server transforms of the 400 components | 386.1 ms | 339.9 ms
(−12%) |
| Vite build of react-data-grid's `src`, which reads no module from disk
| 20.7 ms | 21.0 ms |

`tokens.ts` is now read and parsed once per build and per cold dev
server, instead of 400 times.

**Other effects**

- A read that fails is cached like a parse, until `buildEnd` or
`watchChange` clears it. It fails the transform that interpolates the
module's values either way.
- In the dev server, a read that finishes after `watchChange` forgot the
module can no longer store its stale parse, as the pending parse is
cached before the read starts.

**Alternative**

`parse` from `rolldown/utils`, which parses on another thread, was also
measured. On its own, it gave up to 11% on these builds, the most where
the duplicate parses overlapped, and 0–4% on cold dev transforms. On top
of this branch, it gave at most 4%, while Rolldown advises preferring
`parseSync`, so it's left out.

The tests pass unchanged, with the same uncovered lines and branches as
#127. `AGENTS.md` now says why the parse is cached while pending.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant